// Copyright 2009 Jan Hosang <jan.hosang@gmail.com>. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package recipes

import (
    "os"
    "fmt"
    "strings"
    "container/list"
    
    "gb/depgraph"
    "gb/recipe"
    "gb/invocation"
    "gb/recipes/builtin"
)

/*
 * Package recipes provides a way to create nodes (and edges) for the
 * dependency graph.
 *
 * Recipes (built-in or loaded from files) are used to determine how to
 * generate certain targets and what other targets that process depends on.
 */

const (
    MATCH_NONE = iota
    MATCH_EXACT
    MATCH_VARIABLE
    MATCH_PERCENT
)

type Recipes struct
{
    recipes map[string] *list.List
    dir string
    invAcc *invocationAccumulator
}


func New() *Recipes {
    recp := new(Recipes)
    recp.recipes = make(map[string] *list.List)
    recp.invAcc = newInvocationAcc()
    return recp
}

func (recipes *Recipes) AddRecipes(rs *list.List) {
    for rEl := range rs.Iter() {
        r := rEl.(*recipe.Recipe)
        recipes.AddRecipe(r)
    }
}

func (recipes *Recipes) AddRecipe(r *recipe.Recipe) {
    recps, ok := recipes.recipes[r.Label]
    if !ok {
        recps = list.New()
        recipes.recipes[r.Label] = recps
    }
    recps.PushBack((interface{})(r))
}

func (recipes *Recipes) Invoke(inv *invocation.Invocation, depgraph *depgraph.DependencyGraph) *Dependency {
    if inv.Label == "SubDir" {
        recipes.dir = inv.Targets.Front().Value.(string)
        return nil
    }
    invs := list.New()
    invs.PushBack((interface{})(inv))
    recipes.invoke(invs)
    return nil
}

func (recipes *Recipes) invoke(invs *list.List) {
    vars := make(map[string] *list.List)
    
    var newRecps *list.List
    for invEl := range invs.Iter() {
        inv := invEl.(*invocation.Invocation)
        inv = inv.ReplaceVariables(false, "", vars)
        newRecps = nil
        
        // handle special rules which affect the matching behaviour
        if inv.Label == "GoImports" {
            firstTarget := inv.Targets.Front().Value.(string)
            if inv.Targets.Len() != 1 || firstTarget[0] != '$' {
                fmt.Printf("Warning: Recipe `GoImports' expects exactly one variable as target.\n")
            } else {
                varname := firstTarget[2:len(firstTarget)-1]
                vars[varname] = builtin.GoImports(inv.Sources)
            }
            continue
        }

        // handle rules that induce edges in the dependency graph
        if isDependency(inv.Label) {
        }
        
        rpool, ok := recipes.recipes[inv.Label]
        if !ok {
            fmt.Printf("Fatal Error: Could not find any recipe for `%s'.\n", inv.Label)
            os.Exit(1)
        }
        
        fmt.Printf("\n----------\n\ntry to match:\n%v\n\n", inv)
        firstTarget := inv.Targets.Front().Value.(string)
        if inv.Targets.Len() == 1 && firstTarget[0] == '$' {
            // The matching rules have to return the targets, because the invocation
            // has a variable as target. The only possibility is to match the
            // sources to a recipe.
            varname := firstTarget[2:len(firstTarget)-1]
            var targets *list.List
            targets, newRecps = recipes.matchDifferentSources(rpool, inv.Sources)
            vars[varname] = targets
        } else if inv.Targets.Len() == 1 {
            newRecps = list.New()
            newRecps.PushBack(recipes.matchOneRecipeOneTarget(rpool, firstTarget, inv.Sources))
        } else if inv.Sources.Len() == 1 {
        } else {
            fmt.Printf("Fatal error: Don't know how to match invocation `%s'.\n", inv.String())
            os.Exit(1)
        }
        
        if newRecps == nil {
            fmt.Printf("Fatal Error: Could not find any recipe for `%s'.\n", inv)
            os.Exit(1)
        }
        for recpEl := range newRecps.Iter() {
            recp := recpEl.(*recipe.Recipe)
            fmt.Printf("Match:\n%v", recp)
            if recp.IsLeaf {
                fmt.Printf("Leaf. Add to invocation accumulator.\n\n")
                recipes.invAcc.Add(recp)
            } else {
                recp = recp.ReplaceVariables()
                fmt.Printf("No Leaf. Replacing Variables.\n%v\n", recp)
                recipes.invoke(recp.Invocations)
            }
        }
    }
}

// Find the first recipe in the list, that cooks targets from sources.
// Returns an appropriate invocation.
//
// Example 1:n invocation:
//     Library foo.a: bar.go baz.c
//
// Can match
//     Library %.a: $(<)
//     Library foo.a: %.go %.c          # meaning sources can be .go and .c files
//     Library foo.a: $(<)
//     Library $(>): $(<)
//     Library $(>): %.go %.c
// but not
//     Library %.a: %.go %.c            # %'s value caries from left to right
//     Library foo.a: %.go              # .c files not allowed as source
//     Library foo.a foo.so: %.go %.c   # too many targets
func (recipes *Recipes) matchOneRecipeOneTarget(rcps *list.List, target string, sources *list.List) *recipe.Recipe {
    for rcpEl := range rcps.Iter() {
        rcp := rcpEl.(*recipe.Recipe)
        if rcp.Targets.Len() != 1 {
            continue
        }
        rcpTarget := rcp.Targets.Front().Value.(string)
        // try to match the target
        if repl, varname, matchType, matched := match(rcpTarget, target, true); matched {
            rcpSources := rcp.Sources
            
            // if the matching involves matching a %, then carry it's value
            // to the source list
            if matchType == MATCH_PERCENT {
                rcpSources = list.New()
                for sourceEl := range rcp.Sources.Iter() {
                    source := sourceEl.(string)
                    rcpSources.PushBack((interface{})(invocation.ReplacePerc(source, repl)))
                }
            }
            
            // matching can bind variables and %
            percUsed := false
            percRepl := ""
            vars := make(map[string] *list.List)
            if matchType == MATCH_VARIABLE {
                vars[varname] = list.New()
                vars[varname].PushBack((interface{})(repl))
            }
            
            // each of invocation's sources have to match a source of the
            // recipe
            allSourcesMatch := true
            for sourceEl := range rcpSources.Iter() {
                rcpSource := sourceEl.(string)
                thisSourceMatches := false
                for sourceEl := range sources.Iter() {
                    invSource := sourceEl.(string)
                    if repl, varname, matchType, matched := match(rcpSource, invSource, true); matched {
                        thisSourceMatches = true
                        switch(matchType) {
                        case MATCH_PERCENT:
                            if percUsed && percRepl != repl {
                                thisSourceMatches = false
                            } else {
                                percUsed = true
                                percRepl = repl
                            }
                        case MATCH_VARIABLE:
                            vals, ok := vars[varname]
                            if !ok {
                                vals = list.New()
                                vars[varname] = vals
                            }
                            vals.PushBack(repl)
                        }
                        break
                    }
                }
                if !thisSourceMatches {
                    allSourcesMatch = false
                    break
                }
            }
            if allSourcesMatch {
                targets := list.New()
                targets.PushBack(target)
                rcp.SetVariables(targets, sources, percUsed, percRepl, vars)
                return rcp
            }
        }
    }
    
    fmt.Printf("Fatal Error: Could not find a recipe with label `%s' and target `%s'.\n",
        rcps.Front().Value.(*recipe.Recipe).Label, target)
    fmt.Printf("    Considered recipes:\n")
    for rcpEl := range rcps.Iter() {
        rcp := rcpEl.(*recipe.Recipe)
        if rcp.Targets.Len() != 1 {
            continue
        }
        fmt.Printf("        %v\n", rcp.Header())
    }
    os.Exit(1)
    return nil
}

// Find the first recipe in the list, that cooks targets from sources.
// Returns an appropriate invocation.
//
// Example n:1 invocation:
//     Create foo.cgo1.go foo.cgo2.c: foo.cgo
//
// Can match
//     Create %.cgo1.go %.cgo2.c: %.cgo
//     Create %.cgo1.go %.cgo2.c: $(<)
//     Create $(>): %.cgo
//     Create $(>): $(<)
func matchOneRecipeOneSource(rcps *list.List, targets *list.List, source string) *invocation.Invocation {
    return nil
}

// Match each source to one recipe and return targets for these matches.
//
// Example invocation:
//     Compile $(OBJFILES): foo.go bar.go baz.c
// matches each source to a recipe.
//
// Matching recipes can look like this:
//     Compile _go_.6: %.go
//     Compile _c_.o _go_.6: %.cgo
// or (for another invocation)
//     Create %.c %.go: %.cgo
// but never of the form
//     Binary $(>): $(<)
//
// Resulting invocations might look like this:
//     Compile _go_.6: foo.go bar.go
//     Compile _c_.o: baz.go
// then $(OBJFILES) has to be bound to `_go_.6 _c_.o'.
func (recipes *Recipes) matchDifferentSources(rcps, sources *list.List) (*list.List, *list.List) {
    matchedRecipes := list.New()
    allTargets := make(map[string] bool)
    
    // for each source find the first matching recipe
    for sourceEl := range sources.Iter() {
        source := sourceEl.(string)
        invMatch := false
        for recpEl := range rcps.Iter() {
            recp := recpEl.(*recipe.Recipe)
            // a source matches a recipe if it matches any of the
            // recipe's sources
            for recpSourceEl := range recp.Sources.Iter() {
                pattern := recpSourceEl.(string)
                if percRepl, varname, matchType, ok := match(pattern, source, false); ok {
                    // found matching recipe, build invocation for this match
                    sources := list.New()
                    sources.PushBack(source)
                    vars := make(map[string] *list.List)
                    if matchType == MATCH_VARIABLE {
                        vars[varname] = sources
                    }
                    
                    targets := recp.Targets
                    if matchType == MATCH_PERCENT {
                        targets = list.New()
                        for trg := range recp.Targets.Iter() {
                            targets.PushBack(invocation.ReplacePerc(trg.(string), percRepl))
                        }
                    }
                    
                    recp.SetVariables(
                            targets,
                            sources,
                            matchType == MATCH_PERCENT,
                            percRepl,
                            vars,
                        )
                    matchedRecipes.PushBack(recp)
                    
                    for trg := range recp.Targets.Iter() {
                        allTargets[trg.(string)] = true
                    }
                    
                    invMatch = true
                    break
                }
            }
            if invMatch {
                break
            }
        }
        if !invMatch {
            fmt.Printf("Fatal Error: Could not find a recipe with label `%s' and source `%s'.\n",
                rcps.Front().Value.(*recipe.Recipe).Label, source)
            fmt.Printf("    Considered recipes:\n")
            for recpEl := range rcps.Iter() {
                recp := recpEl.(*recipe.Recipe)
                fmt.Printf("        %v\n", recp.Header())
            }
            os.Exit(1)
        }
    }
    
    matches := list.New()
    for trg, _ := range allTargets {
        matches.PushBack((interface{})(trg))
    }
    return matches, matchedRecipes
}

func match(pattern, str string, matchVariables bool) (string, string, int, bool) {
    // variables match anything (if we want to)
    if pattern[0] == '$' {
        return str, pattern[2:len(pattern)-1], MATCH_VARIABLE, matchVariables
    }
    
    psfix := strings.Split(pattern, "%", 2)
    if len(psfix) == 1 {
        // no % in pattern, try exact matching
        return str, "", MATCH_EXACT, str == pattern
    }
    
    // try %-matching ("prefix%suffix", "%.go")
    if !strings.HasPrefix(str, psfix[0]) || !strings.HasSuffix(str, psfix[1]) {
        return "", "", MATCH_NONE, false
    }
    // slice out what was matched to %
    return str[len(psfix[0]):len(str)-len(psfix[1])], "%", MATCH_PERCENT, true
}

